#!/usr/bin/env python3
# coding=UTF-8
# Copyright (c) 2022 Huawei Technologies Co., Ltd
#
# This software is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

"""
yr api config for user
"""
import logging
import os
import uuid
from dataclasses import dataclass
import functools

from yr import utils
from yr.utils import Singleton

_DEFAULT_CLUSTER_PORT = "31220"
_DEFAULT_IN_CLUSTER_CLUSTER_PORT = "21003"
_DEFAULT_DS_PORT = "31501"
_DEFAULT_DS_PORT_OUTER = "31502"
_URN_LENGTH = 7
_DEFAULT_CONNECTION_NUMS = 100
DEFAULT_CONCURRENCY = 1
DEFAULT_RECYCLE_TIME = 2
_PREFIX_OF_FUNCTION_ID = "sn:cn:yrk:12345678901234561234567890123456:function:0-adminservice-"
_SUFFIX_OF_FUNCTION_ID = ":$latest"
_JOB_ID_LENGTH = 8


@dataclass
class UserTLSConfig:
    """
    Out cluster for user ssl/tls
    """
    root_cert_path: str
    module_cert_path: str
    module_key_path: str
    server_name: str = None


@dataclass
class DeploymentConfig:
    """
    AutoDeploymentConfig

    Attributes:
        cpu(str): cpu acquired, the unit is millicpu
        mem(str): mem acquiored (MB)
        datamem(str): data system mem acquired (MB)
        spill_path(str): spill path, when out of memory will flush data to disk
        spill_size(str): spill size limit (MB)
    """
    cpu: int = 0
    mem: int = 0
    datamem: int = 0
    spill_path: str = ""
    spill_limit: int = 0


@dataclass
class Config:
    """
    yr API config

    Attributes:
        function_id(str): function id which you deploy, get default by env `YRFUNCID`
            etc. sn:cn:yrk:12345678901234561234567890123456:function:0-test-test:$latest.
        auto_function_name(str): use default function which define in admin-service, see the document for details.
        cpp_function_id(str): cpp function id which you deploy, get default by env `YR_CPP_FUNCID`.
        cpp_auto_function_name(str): use default function for cpp.
        function_name(str): function name which need in runtime.
        server_address(str): System cluster address, get default by env `YR_SERVER_ADDRESS`.
        ds_address(str): DataSystem address, get default by env `YR_DS_ADDRESS`.
        app_id(str): System cluster app id, required when authentication
            is enabled.
        app_key(str): System cluster app key, required when authentication
            is enabled.
        on_cloud(bool): only True when initialize in runtime.
        log_level: yr api log level:ERROR/WARNING/INFO/DEBUG, default: WARNING.
        invoke_timeout(int): http client read timeout(sec), default 900.
        local_mode(bool): run code in local.
        code_dir(str): need set which init in runtime.
        connection_nums(int): http client connection nums, default: 100, limit: [1,∞).
        recycle_time(int): instance recycle period(sec), default: 2 second, limit: (0,300].
        in_cluster: if True will use datasystem in cluster client
        job_id(str): auto generated by init.
        tls_config(UserTLSConfig): for out cluster https ssl
        auto(bool): auto start distribute-executor when yr.init, and auto stop distribute-executor when yr.finalize.
        deployment_config(DeploymentConfig): when auto=True needed, use to define deployment detail.
    """
    function_id: str = ""
    auto_function_name: str = ""
    cpp_function_id: str = ""
    cpp_auto_function_name: str = ""
    function_name: str = ""
    server_address: str = ""
    ds_address: str = ""
    app_id: str = ""
    app_key: str = ""
    on_cloud: bool = False
    log_level: (str, int) = logging.WARNING
    invoke_timeout: int = 900
    local_mode: bool = False
    code_dir: str = ""
    connection_nums: int = _DEFAULT_CONNECTION_NUMS
    recycle_time: int = DEFAULT_RECYCLE_TIME
    in_cluster: bool = False
    job_id: str = ""
    tls_config: UserTLSConfig = None
    auto: bool = False
    deployment_config: DeploymentConfig = None


def _get_from_env(conf):
    if conf.function_id == "":
        conf.function_id = os.environ.get("YRFUNCID", "")
    if conf.cpp_function_id == "":
        conf.cpp_function_id = os.environ.get("YR_CPP_FUNCID", "")
    if conf.server_address == "":
        conf.server_address = os.environ.get("YR_SERVER_ADDRESS", "")
    if conf.ds_address == "":
        conf.ds_address = os.environ.get("YR_DS_ADDRESS", "")
    return conf


def _check_function_urn(on_cloud, value):
    if value is None:
        value = ''
    if on_cloud and value == '':
        return False
    items = value.split(':')
    if len(items) != _URN_LENGTH:
        raise ValueError("invalid function id")
    return True


def _get_function_id_by_function_name(function_name, function_id):
    """Get function id by auto function"""
    if function_name is None or function_name == "":
        return function_id
    return _PREFIX_OF_FUNCTION_ID + function_name + _SUFFIX_OF_FUNCTION_ID


@dataclass
class ClientInfo:
    """
    Use to storage yr client info
    """
    job_id: str


@dataclass
class FunctionInfo:
    """
    Use to storage yr function info
    """
    function_id: str
    function_name: str
    app_id: str
    app_key: str


@Singleton
class ConfigManager:
    """
    Config manager singleton

    Attributes:
        function_id: function id which you deploy
        server_address: System cluster address.
        ds_address: DataSystem address.
        app_id: System cluster app id, required when authentication
            is enabled.
        app_key: System cluster app key, required when authentication
            is enabled.
        on_cloud: only True when initialize in runtime
        log_level: yr api log level, default: WARNING
    """

    def __init__(self):
        self.__function_id = None
        self.__all_function_id = {}
        self.__server_address = None
        self.__ds_address = None
        self.__recycle_time = None
        self.__connection_nums = None
        self.__runtime_id = None
        self.app_id = None
        self.app_key = None
        self.is_init = False
        self.on_cloud = False
        self.__log_level = logging.WARNING
        self.job_id = None
        self.function_name = None
        self.invoke_timeout = None
        self.local_mode = False
        self.code_dir = None
        self.__in_cluster = False
        self.__tls_config = None
        self.__auto = False
        self.__deployment_config = DeploymentConfig()

    @property
    def auto(self) -> bool:
        """ is auto deployment mode """
        return self.__auto

    @property
    def deployment_config(self) -> DeploymentConfig:
        """ when auto=True needed, use to define deployment detail. """
        return self.__deployment_config

    @property
    def in_cluster(self) -> bool:
        """if True will use datasystem in cluster client"""
        return self.__in_cluster

    @property
    def log_level(self):
        """
        YR api log level
        """
        return self.__log_level

    @log_level.setter
    def log_level(self, value):
        """
        YR api log level
        """
        if isinstance(value, str):
            value = value.upper()
        self.__log_level = value

    @property
    def function_id(self):
        """
        Get function id
        """
        return self.__function_id

    @function_id.setter
    def function_id(self, value: str):
        """
        Set function id

        Args:
            value (str): The function id which user deploy
        """
        if _check_function_urn(self.on_cloud, value):
            self.__function_id = value
            self.function_name = utils.get_function_from_urn(value)

    @property
    def all_function_id(self):
        """
        Get all function id
        """
        return self.__all_function_id

    @all_function_id.setter
    def all_function_id(self, value: dict):
        """
        Set function id dict
        <key, value> example: <"cpp", "sn:cn:yrk:12345678901234561234567890123456:function:0-test-cpp:$latest">

        Args:
            value (dict): The function id dictionary
        """
        for urn in value.values():
            if urn != '':
                _check_function_urn(self.on_cloud, urn)

        self.__all_function_id = value

    @property
    def server_address(self):
        """
        Get server address
        """
        return self.__server_address

    @server_address.setter
    def server_address(self, value: str):
        """
        Set server address

        Args:
            value (str): System cluster ip
        """
        if self.on_cloud:
            return
        if utils.validate_ip(value):
            if self.__in_cluster:
                self.__server_address = value + ":" + _DEFAULT_IN_CLUSTER_CLUSTER_PORT
            else:
                self.__server_address = value + ":" + _DEFAULT_CLUSTER_PORT
            return
        _, _ = utils.validate_address(value)
        self.__server_address = value

    @property
    def ds_address(self):
        """
        Get datasystem address
        """
        return self.__ds_address

    @ds_address.setter
    def ds_address(self, value: str):
        """
        Set datasystem address

        Args:
            value (str): Datasystem worker address : <ip>:<port> or <ip>,
                default port : 31501
        """
        if self.on_cloud:
            return
        if utils.validate_ip(value):
            if self.on_cloud or self.__in_cluster:
                self.__ds_address = value + ":" + _DEFAULT_DS_PORT
            else:
                self.__ds_address = value + ":" + _DEFAULT_DS_PORT_OUTER
            return
        _, _ = utils.validate_address(value)
        self.__ds_address = value

    @property
    def connection_nums(self):
        """
        Get connection_nums
        """
        return self.__connection_nums

    @connection_nums.setter
    def connection_nums(self, value: int):
        """
        Set connection_nums

        Args:
            value (int): max connection number
        """
        if not isinstance(value, int):
            raise TypeError(f"connection_nums {type(value)} type error, 'int' is expected.")
        if (value >= 1) is False:
            raise ValueError(f"invalid connection_nums value, expect connection_nums >= 1, actual {value}")

        self.__connection_nums = value

    @property
    def recycle_time(self):
        """
        Get recycle time
        """
        if self.__recycle_time:
            return self.__recycle_time
        return DEFAULT_RECYCLE_TIME

    @recycle_time.setter
    def recycle_time(self, value: int):
        """
        Set recycle time

        Args:
            value (int): instance recycle period
        """
        if not isinstance(value, int):
            raise TypeError(f"recycle_time {type(value)} type error, 'int' is expected.")
        if (1 <= value <= 300) is False:
            raise ValueError(f"invalid recycle_time value, expect 1 <= time <= 300, actual {value}")

        self.__recycle_time = value

    @property
    def runtime_id(self):
        """
        Get runtime id
        """
        return self.__runtime_id

    @runtime_id.setter
    def runtime_id(self, value: str):
        """
        Set runtime id

        Args:
            value (str): runtime id
        """
        self.__runtime_id = value

    @property
    def tls_config(self):
        """
        Get tls config
        """
        return self.__tls_config

    def init(self, conf: Config):
        """
        Init the ConfigManager

        Args:
            conf (Config): The yr api config which set by user.
        """
        conf = _get_from_env(conf)
        self.__auto = conf.auto
        self.__deployment_config = conf.deployment_config
        self.connection_nums = conf.connection_nums
        self.log_level = conf.log_level
        self.invoke_timeout = conf.invoke_timeout
        self.__tls_config = conf.tls_config
        if conf.job_id != "":
            self.job_id = conf.job_id
        else:
            # Use 8-bit uuid to reduce the length of requestID
            self.job_id = "job-" + str(uuid.uuid4().hex)[:9]
        utils.set_job_id(self.job_id)
        self.recycle_time = conf.recycle_time
        self.local_mode = conf.local_mode
        if self.local_mode:
            return
        self.on_cloud = conf.on_cloud
        self.__in_cluster = conf.in_cluster
        conf.function_id = _get_function_id_by_function_name(conf.auto_function_name, conf.function_id)
        conf.cpp_function_id = _get_function_id_by_function_name(conf.cpp_auto_function_name, conf.cpp_function_id)
        self.function_id = conf.function_id
        if conf.function_id is None or conf.function_id == "":
            self.function_name = conf.function_name

        self.all_function_id = {
            utils.LANGUAGE_CPP: conf.cpp_function_id,
            utils.LANGUAGE_PYTHON: conf.function_id,
        }
        self.server_address = conf.server_address
        self.ds_address = conf.ds_address
        self.code_dir = conf.code_dir
        if conf.app_id == "":
            self.app_id = "accessservice"
        else:
            self.app_id = conf.app_id
        if conf.app_key == "":
            self.app_key = "a9abff86a849f0d40f5a399252c05def4d744a28d3ad27fd73c80db11b706ac8"
        else:
            self.app_key = conf.app_key

    def get_function_info(self):
        """
        Get function info which user deploy
        """
        return FunctionInfo(function_id=self.function_id, function_name=self.function_name,
                            app_id=self.app_id, app_key=self.app_key)

    def get_function_id_by_language(self, language):
        """
        Get function id by language from function id dict

        Args:
            language (str): The language of target function or class
        """
        return self.__all_function_id.get(language)


def check_init(func):
    """
    The decorator to check whether yr api init.
    """

    @functools.wraps(func)
    def wrapper(*args, **kw):
        if not ConfigManager().is_init:
            raise RuntimeError("system not initialized")
        return func(*args, **kw)

    return wrapper
